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
Related
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
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
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.
I want design page like this
up to now I have created like this . I want to know bind check-box front of each row and send those checked/non-checked values with IDs using json and jquery
this last code snippet of that page
<div style="width:50%; float:left;text-align:left"><button id="resetborchure" type="button" class="btn btn-warning submit">Reset Brochure</button> </div>
<div style="width:50%; float:left;text-align:right"><button id="createborchure" type="button" class="btn btn-danger submit">Create Brochure</button> </div>
<script type="text/javascript">
</script>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
#Scripts.Render("~/bundles/jqueryui")
<script type="text/javascript">
var url = '#Url.Action("FetchProductProperties")';
var editUrl = '#Url.Action("Edit")';
var type = $('#Type');
var category = $('#Category');
var country = $('#Country');
var product = $('#Product');
var template = $('#template');
var table = $('#table');
$('#search').click(function () {
table.empty();
$.getJSON(url, { type: type.val(), category: category.val(), country: country.val(), product: product.val() }, function (data) {
$.each(data, function (index, item) {
var clone = template.clone();
var cells = clone.find('td');
cells.eq(0).text(item.ID);
cells.eq(1).text(item.Name);
table.append(clone.find('tr'));
});
});
});
$('#resetborchure').click(function () {
table.empty();
});
</script>
}
Also I want , once I checked and click create brochure button I want send those checked/non-checked values with IDs using json
I have try to put to populate a checkbox with each listed result '<td><input type="checkbox" /></td>' inside cells.eq(1).text(item.Name);
as cells.eq(1).text('<td><input type="checkbox" /></td>'+item.Name); but this is not working
Once I click "Select Information" button Its list down data from AB_Product_Property table , IF I want to populate check-box with each search result row Do I need maintain boolean field in that table also ?? I want to do this without maintain column for that boolean field in AB_Product_Property table
Create a view model(s) to represent what you want to display/edit.
public class OptionsVM
{
public int ID { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
public class SearchVM
{
public int Asset { get; set; }
public SelectList AssetList { get; set; }
public int Category{ get; set; }
public SelectList CategoryList { get; set; }
.... // other properties and SelectLists for the dropdownlists
public List<OptionsVM> Options { get; set; }
}
and in the GET method, populate the Options with the ID and Name properties
Then in the view
#model SearchVM
....
<form>
#Html.DropDownListFor(m => m.Asset, Model.AssetList)
....
#for(int i = 0; i < Model.Options.Count; i++)
{
#Html.HiddenFor(m => m.Options[i].ID)
#Html.CheckBoxFor(m => m.Options[i].IsSelected)
#Html.LabelFor(m => m.Options[i].IsSelected, Model.Options[i].Name)
}
<button type="button" id="createbrochure">Create Brochure</button>
and in the script
$('#createbrochure').click(function () {
$.getJSON(url, $('form').serialize(), function (data) {
....
});
})
and in the controller method
public ActionResult CreateBrochure(SearchVM model)
{
// To get the ID's of all selected options
IEnumerable<int> selectedOptions = model.Options.Where(o => o.IsSelected).Select(o => o.ID);
....
}
You can create new column for check box instead of appending check box to name column.
Than you can set class to that check box and get the checked or unchecked check box value using jquery.
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);
}