View not updating after using js.InvokeAsync in Blazor - javascript

This code doesn't seem to remove the movie from the UI. The model does change. Only if I explicitly call StateHasChanged() will the UI change too.
Code:
#code{
string name = "Guy";
public bool display { get; set; } = false;
[Parameter] public List<Movie> Movies { get; set; }
private async void Delete(Movie movie)
{
bool confirm = await js.InvokeAsync<bool>("confirm", "Sure?");
if (confirm == true)
{
Movies.Remove(movie);
Console.WriteLine(Movies.Count);
//StateHasChanged();
}
}
}
And UI is simple as that:
#inject IJSRuntime js
<h1>Hello, world!</h1>
Welcome ,#StringsHelpers.toUpper(name) , to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div>
<h3>Movie</h3>
<input type="checkbox" #bind="display" />
<GenericList List="Movies">
<ElementTemplate>
<IndMovie IsDisplayed="display" Movie="context" Delete="Delete"></IndMovie>
</ElementTemplate>
</GenericList>
<div>
#foreach (var item in Movies)
{
#item.Title
}
</div>
<br />
</div>

Seems like the private async void Delete(Movie movie) should return Task instead

try this:
public async Task Delete(Movie movie)
{
bool confirm = await js.InvokeAsync<bool>("confirm", "Sure?");
if (confirm == true)
{
Movies.Remove(movie);
Console.WriteLine(Movies.Count);
//StateHasChanged();
}
}
Other problem can be if the List of Movies not update de state in the parent.
If the code donĀ“t make you try to put Delete method in the parent component and pass this like parameter

Related

ElementReference reverts to null on repaint

I'm trying to implement a Tabbed sections style of navigation e.g. a NavBar displays a list of section names, and clicking one one jumps down the page. I've managed to store my section headers in an array of ElementReference, and on first click on a header, everything works and my Javascript portion receives a DOMReference for the section.
If the page rerenders, or even if a Component rerenders, then the references are wiped out. I'm not clear on where/how these should be stored during repaint such that they aren't null in my event.
#using CustomStyle.Client.Code
#using models = CustomStyle.Shared.Models
#using clientmodels = CustomStyle.Client.Models
#using demomodels = CustomStyle.Shared.Models.Demo
#using System.Linq;
#inject IJSRuntime JSRuntime
<header #ref="navbarRef" class="navbar">
<SfMenu TValue="MenuItem" EnableScrolling=#true>
<MenuEvents TValue="MenuItem" ItemSelected="#(e => PanelMenuClick(e))" />
<MenuItems>
#{
int panIndex = 0;
foreach(var panel in Data.Sections) {
<MenuItem Text=#panel.Title Id=#($"{idPrefix}-{panIndex++}") />
}
}
</MenuItems>
</SfMenu>
</header>
<article class="st-vscroll bg-body pb-5">
#{
int sectIndex = 0;
sectionRefs = new ElementReference[Data.Sections.Count];
foreach(var panel in Data.Sections) {
var thisPanIndex = sectIndex++;
<div #ref="sectionRefs[thisPanIndex]">
<SectionHeader Title=#panel.Title />
</div>
<!-- MORE SECTION CONTENT -->
}
}
</article>
#code {
[Parameter] public clientmodels.SectionAndPanelLayoutData Data { get; set; } = null;
[Parameter] public string idPrefix { get; set; } = "panel";
private ElementReference[] sectionRefs { get; set; } = null;
private ElementReference navbarRef;
protected override void OnInitialized()
{
base.OnInitialized();
}
//protected override void OnParametersSet()
//{
// sectionRefs = new ElementReference[Data.Sections.Count];
//}
private async Task PanelMenuClick(MenuEventArgs<MenuItem> e)
{
var thisPanIndex = int.Parse(e.Item.Id.Substring(idPrefix.Length + 1));
await JSRuntime.InvokeVoidAsync("JSFuncs.PanelLayout_jumpToHeader", navbarRef, sectionRefs[thisPanIndex]);
}
}
Typescript for JS portion if that's the problem - although I'm pretty sure that the sectionElement parameter comes through as null, since they are getting wiped in the Blazor bit:
namespace JSFuncs {
function getScrollParent(node) {
if (node == null) {
return null;
}
if (node.scrollHeight > node.clientHeight) {
return node;
} else {
return getScrollParent(node.parentNode);
}
}
export function PanelLayout_jumpToHeader(navbarElement: HTMLElement, sectionElement: HTMLElement): void {
//Need to work out height of panel view navbar
var navbarheight = navbarElement.offsetHeight;
//Need to call scrollIntoView for sectionElement
sectionElement.scrollIntoView();
//Need to find scroll container
var scrollContainer = getScrollParent(sectionElement);
//Need to scroll back up by height of navbar
var scrolledY = scrollContainer.scrollTop;
if (scrolledY) {
scrollContainer.scroll(0, scrolledY > navbarheight ? scrolledY - navbarheight : 0);
}
}
}
In the code, the first call to PanelMenuClick has the references set, but repainting a child component seems to break the refs. I also tried to reallocate my array OnSetParameters which also doesn't work.
What's the correct pattern to store a variable number of references in Blazor?
Thanks.
Mark

Blazor rendering twice when Task with IJSRuntime method is called

I've got this test program I'm building to learn Blazor WASM, and I've built a generic table that takes a render fragment that runs a function on a given row. This function is just a simple JS alert that shows the details of a field of the specified row.
My problem is that this call causes the UI to render twice (I don't want it to render at all). I have found a work around at the moment by using an override for ShouldRender() and switching the render boolean to false both before, and after the JS call. But, I don't really like this approach.
Does anyone have any insight into why this is happening? Can I avoid having to do this with a different approach?
Here's the method, It's in a parent component to the table. You can see the boolean being set twice. This matters because the Table can be sorted, and I lose the sort whenever the alert is clicked (If I don't set the boolean twice, that is. If I set it once before or after the JS call it re renders once, and if I don't set it at all it renders twice).
`private async Task HandleClick(WeatherForecast forcast)
{
Render = false;
await js.InvokeVoidAsync("alert", $"{forcast.TemperatureF}");
Render = false;
}`
Here's the action that calls the method
<button #onclick="#(() => SelectedItem.InvokeAsync(item))">#renderFragment</button>
#typeparam GenericType
#code {
[Parameter] public EventCallback<GenericType> SelectedItem { get; set; }
[Parameter] public RenderFragment renderFragment { get; set; }
[Parameter] public GenericType item { get; set; }
}
And here's the table where the button is clicked.
#using System.ComponentModel;
#using System.Reflection;
#typeparam GenericType
#if (Items == null)
{
<img src="https://media4.giphy.com/media/3oEjI6SIIHBdRxXI40/giphy.gif" />
}
else
{
<table class="table">
<thead>
<tr>
#foreach (var prop in Props)
{
//Find the displayname, if it exists.
var name = PDesctirptions.Find(prop.Name, true)?.DisplayName ?? prop.Name;
<td #onclick="#(() => OrderBy(prop.Name))">#name</td>
}
#* if we have actions, show them. *#
#if(ActionFragment != null)
{
<td>Actions</td>
}
</tr>
</thead>
<tbody>
#foreach (var item in Items)
{
<tr>
#foreach (var p in Props)
{
//Get's the property value of the item to display in the table.
<td>#item.GetType().GetProperty(p.Name).GetValue(item).ToString()</td>
}
#if (ActionFragment != null)
{
<td>#ActionFragment(item)</td>
}
</tr>
}
</tbody>
</table>
}
#code {
private bool desc = false;
[Parameter] public IEnumerable<GenericType> Items { get; set; }
[Parameter] public RenderFragment<GenericType> ActionFragment { get; set; }
private Type T = typeof(GenericType);
private PropertyInfo[] Props { get; set; }
private PropertyDescriptorCollection PDesctirptions { get; set; }
protected override void OnInitialized()
{
Props = T.GetProperties();
PDesctirptions = TypeDescriptor.GetProperties(T);
}
void OrderBy(string name)
{
PropertyDescriptor pDescriptor = TypeDescriptor.GetProperties(T).Find(name, true);
if (desc)
{
Items = Items.OrderBy(d => pDescriptor.GetValue(d)).ToList();
desc = false;
}
else
{
Items = Items.OrderByDescending(d => pDescriptor.GetValue(d)).ToList();
desc = true;
}
}
}
Here's a visualization of the table, if it helps.
enter image description here

Populate DropDownList from another DropDownList

I have two related models.
public partial class bs_delivery_type
{
public decimal delivery_id { get; set; }
public decimal delivery_city_id { get; set; }
public string delivery_address { get; set; }
public virtual bs_cities bs_cities { get; set; }
}
and the second one:
public partial class bs_cities
{
public bs_cities()
{
this.bs_delivery_type = new HashSet<bs_delivery_type>();
}
public decimal cities_id { get; set; }
public string cities_name { get; set; }
public virtual ICollection<bs_delivery_type> bs_delivery_type { get; set; }
}
and I have such ViewBag's for dropdownlist's:
ViewBag.city = new SelectList(_db.bs_cities, "cities_id", "cities_id");
ViewBag.delivery_adress = new SelectList(_db.bs_cities, "delivery_id", "delivery_address");
When I choose city in first dropdownlist, in the second one there has to be appeared binded list with delivery_adress, where delivery_city_id = cities_id(from first dropdownlist).
How to do that?
Edit:
I tryed method from #Izzy's comment, so here is my actual view:
#model Bike_Store.Models.DeliveryModel
#{
ViewBag.Title = "Checkout";
}
<script src="~/Scripts/bootstrap.min.js"></script>
<script src="~/Scripts/jquery-3.1.1.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"></script>
<script type="text/javascript">
function GetDelivery(_stateId) {
var procemessage = "<option value='0'> Please wait...</option>";
$("#ddldelivery").html(procemessage).show();
var url = "/Shop/GetDeliveryByCityId/";
$.ajax({
url: url,
data: { cities_id: _stateId },
cache: false,
type: "POST",
success: function (data) {
var markup = "<option value='0'>Select adress</option>";
for (var x = 0; x < data.length; x++) {
markup += "<option value=" + data[x].Value + ">" + data[x].Text + "</option>";
}
$("#ddldelivery").html(markup).show();
},
error: function (reponse) {
alert("error : " + reponse);
}
});
}
</script>
<h2>Checkout</h2>
#using (Html.BeginForm())
{
#Html.DropDownListFor(m=>m.CitiesModel, new SelectList(Model.CitiesModel, "cities_id", "cities_name"), new {#id = "ddldelivery", #style="width:200px", #onchange="javascript:GetDelivery(this.value);"})
<br />
<br />
<select id="ddldelivery" name="ddldelivery" style="width:200px">
</select>
<br /><br />
}
My controller now looks like this:
public List<bs_cities> GetAllCities()
{
List<bs_cities> cities = new List<bs_cities>();
foreach (var city in _db.bs_cities)
{
cities.Add(city);
}
return cities;
}
public List<bs_delivery_type> GetAllDeliveries()
{
List<bs_delivery_type> deliveries = new List<bs_delivery_type>();
foreach (var delivery in _db.bs_delivery_type)
{
deliveries.Add(delivery);
}
return deliveries;
}
[HttpPost]
public ActionResult GetDeliveryByCityId(decimal cities_id)
{
List<bs_delivery_type> delivery = new List<bs_delivery_type>();
delivery = GetAllDeliveries().Where(m => m.delivery_city_id == cities_id).ToList();
SelectList objDelivery = new SelectList(delivery, "delivery_id", "delivery_address", 0);
return Json(objDelivery);
}
public ViewResult Checkout()
{
DeliveryModel deliveryModel = new DeliveryModel();
deliveryModel.CitiesModel = new List<bs_cities>();
deliveryModel.CitiesModel = GetAllCities();
return View(deliveryModel);
}
The problem now is that i have 2 ddls, but works only first one.
In scrshot you can see I have a list of cities, when I choose a city, in this same ddl appears a list of delivery adresses, and when I choose adress - its desappears. What a magic? Help me please with Ajax.
List of cities
I guesse i fixed it, the problem was in:
#Html.DropDownListFor(m=>m.CitiesModel, new SelectList(Model.CitiesModel, "cities_id", "cities_name"), new {#id = "ddldelivery", #style="width:200px", #onchange="javascript:GetDelivery(this.value);"})
I changes #id = "ddldelivery" to #id = "ddlcity" and it works now
The following guide will show you:
Create a partial view
Takes cityid as input and outputs the delivery address list
Load partial view into your select
Note: Partial view solution may be overkill in this situation, but for similar problems it is actually quite usefull.
PartialView .cshtml
Filename: _deliveryTypePartial.cshtml
#model List<bs_delivery_type>
#foreach(var item in Model)
{
<option value="#item.delivery_id">
#item.delivery_address
</option>
}
Controller Code for Partial View:
public IActionResult _deliveryTypePartial(decimal city_id)
{
List<bs_delivery_type> model = context.bs_delivery_types.Where(row => row.delivery_city_id == delivery_city_id).ToList();
return PartialView(model);
}
And then Finally, for your AJAX
I notice that your two dropdownlists have identical ID's witch will cloud your javascript code and is considered bad practice, so for the purposes of this guide I will call the first dropdownlist:
ddlcity
Now, inside your onchange function for ddlcity:
$('#ddldelivery').load("/ControllerName/_deliveryTypePartial?city_id=" _stateId);
This should load the partial view into your second dropdown list.
PS: As I completed this question you had already used the direct ajax method, I agree that both methods are equally suitable in this case. You can perhaps use the method outlined here if the actual objects you need to populate are a lot more complex.

How to add onclick event to radio button in Razor View

I have this view. It's ok besides that I want to add: onclick="javascript:SubmitClick(#Model.id, answer.Id);" to definition of RadioButtons in it.
#model WebApplication2.Models.Question
<div>
#Html.HiddenFor(x => x.Id)
<h3> #Model.QuestionText </h3>
#foreach (var answer in Model.Answers) {
<p>
#Html.RadioButtonFor(b => b.SelectedAnswer, answer.Id) #answer.AnswerText
</p>
}
</div>
Question and Answer are models which look like that.
public class Question {
public int Id { set; get; }
public string QuestionText { set; get; }
public virtual ICollection<Answer> Answers { set; get; }
public string SelectedAnswer { set; get; } //this field is SET after clicking the radio button, I don't need this field ant this behaviour
}
public class Answer {
public int Id { set; get; }
public string AnswerText { set; get; }
}
As far as I know b => b.SelectedAnswer this expression inside #Html.RadioButtonFor makes that after click the field SelectedAnswer in class Question will be set. I don't need that I only need that only one of those radio buttons inside foreach loop can be selected at the same time and clicking on them invokes onclick="javascript:SubmitClick(#Model.id, answer.Id);". Also it can be done in obsolete way, just to make it work.
Screen which will help undestand how the view looks:
The view:
#model WebApplication2.Models.Question
<div>
#Html.HiddenFor(x => x.Id)
<h3> #Model.QuestionText </h3>
<form action="">
#foreach (var answer in Model.Answers) {
<p>
<input type="radio" onclick="javascript:SubmitClick(#Model.Id, #answer.Id);">#answer.AnswerText<br>
</p>
}
</form>
</div>
The script function is in strongly typed view against Person:
function SubmitClick(qid, aid) {
var url = '#Url.Action("SubmitSurvey", "Person")';
$.post(url, { questionId: qid, answerId: aid, personId:#Model.Id }, function (data) {
alert('questionId: ' + qid + ' answerId ' + aid);
});
}
Method invoked by script
public void SubmitSurvey(int questionId, int answerId, int personId) {
System.Diagnostics.Debug.WriteLine("UPDATING DATABASE " + "Selected personId: " + personId);
}

struts2 UpDownselect not giving full ordered list on form submit

I am using Struts2 UpDownSelect component.
But when i am submitting the form instead of getting the comma seperated sorted list I am geeting just one element which was last selected in the list.
JSp :
<div class="search-area">
<s:form id="saveTableOrder" method="post" action="save-operator-table-order" name="tableOrderForm" >
<div class="inputField">
<s:select id="operatorSelect" name="selectedOperator" value="selectedOperator" key="operator" list="operators" listKey="operatorName" listValue="operatorName" labelposition="left" onChange="this.form.submit()"/>
</div>
<div class="inputField">
<s:updownselect name="tableOrder" list="tablesOrder" key="manageOrder" moveDownLabel="Down" moveUpLabel="Up" value="tableOrder" labelposition="left" ></s:updownselect>
</div>
<div class="submitButtons">
<s:submit key="general.button.save" />
<input id="operator-table-order-cancel" type="submit" name="action:cancel-operator-table-order" value="<s:property value="getText('general.button.cancel')"/>" />
</div>
</s:form>
</div>
Action Class :
public class ManageTableOrderAction extends BOActionSupport {
private String tableOrder;
private List<OperatorDTO> operators;
private String selectedOperator;
List<String> order = new ArrayList<>();
#Override
#SkipValidation
public String execute() {
init();
return SUCCESS;
}
private void init() {
//something
order.add("one");
order.add("one");
.......
}
public String saveTableOrder() {
if (tableOrder != null && !tableOrder.isEmpty()) {
tableOrder = lobbyTableService.saveTableOrder(selectedOperator, tableOrder);
}
return SUCCESS;
}
public String cancelTableOrder() {
return SUCCESS;
}
public List<OperatorDTO> getOperators() {
init();
return operators;
}
public void setOperators(List<OperatorDTO> operators) {
this.operators = operators;
}
public String getSelectedOperator() {
return selectedOperator;
}
public void setSelectedOperator(String selectedOperator) {
this.selectedOperator = selectedOperator;
}
public String getTableOrder() {
return tableOrder;
}
public void setTableOrder(String tableOrder) {
this.tableOrder = tableOrder;
}
public List<String> getTablesOrder() {
return order;
}
public void setTablesOrder(List<String> tablesOrder) {
this.order = tablesOrder;
}
Now when clicking submit button. My form is submitted but in tableOrder I get only one element of the list.
I was expecting full list in comma seperated form.
I searched a lot no where found anybody having the same issue
Thanks in advance for help.
You need to add <s:head/> tag to your JSP, which adds Struts2 utils.js. This file holds javascript functions which are required for <s:updownselect> tag to work properly.

Categories