ElementReference reverts to null on repaint - javascript

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

Related

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

View not updating after using js.InvokeAsync in Blazor

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

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.

Change color of text in ASP.NET table cell (according to value)

could you please help me with this:
I am creating a table of some items with their properties and values (over some time).
Looks like:
I want to change color of those values, lets say 0 will be green 1 blue 2 red.
DataView dv = new DataView(grid);
myGridView.DataSource = dv;
myGridView.DataBind();
If I need to be more specific, please tell me, I am not a PRO. I don't know where can I control the cell.
I'd also like to add mouse over the text (so it will show the date of occurrence of current value).
Maybe some function/javascript...?
Very simple and light weight solution using jQuery:
Code behind:
public partial class GridViewCellColourChange : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if(!Page.IsPostBack)
{
gvItems.DataSource = this.GetData();
gvItems.DataBind();
}
}
private IEnumerable<Item> GetData()
{
var item1 = new Item { item = "itm1", prop1 = "23", prop2 = "asd", prop3 = "23d", values = "0-0-0-0-0" };
var item2 = new Item { item = "itm2", prop1 = "43", prop2 = "asd", prop3 = "23d", values = "0-0-0-0-0" };
var item3 = new Item { item = "itm3", prop1 = "53", prop2 = "asd", prop3 = "23d", values = "0-0-0-0-0" };
return new List<Item> { item1, item2, item3 };
}
}
public class Item
{
public string item { get; set; }
public string prop1 { get; set; }
public string prop2 { get; set; }
public string prop3 { get; set; }
public string values { get; set; }
}
.ASPX:
<head runat="server">
<title></title>
<script src="https://code.jquery.com/jquery-1.12.2.min.js"></script>
<script>
$(function () {
$("#gvItems > tbody > tr").each(function (i, row) {
var children = $(row).children();
for(var i=0; i < children.length;i++)
{
var child = children[i];
if(child.localName == "td")
{
var text = $(child).text();
if(text == "23")
{
$(child).css("background-color", "red");
}
else if(text == "43")
{
$(child).css("background-color", "green");
}
}
}
});
});
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="gvItems" runat="server"></asp:GridView>
</div>
</form>
</body>
Output:
You can use gridview control with OnRowDataBound property to bind specific properties to the each cell/Row.
Use OnRowDataBoundEvent of gridview,here is sample
protected void GridCompany_OnRowDataBound(object sender, GridViewRowEventArgs e)
{
foreach (GridViewRow gridViewRow in GridCompany.Rows)
{
if (gridViewRow.RowType == DataControlRowType.DataRow)
{
if (gridViewRow.Cells[6].Text.ToUpper() == "TRUE")// find your cell value and check for condition
{
// put here your logic
}
else
{
// put here your Else logic
}
}
}
}

Toolbar is hiding on Scroll upward in listView but don't come back when scrolled downward

In my app i am using the design toolbar. In one activity i am using list view which fetches data from a json and show it. When the user scroll upward the toolbar hides but when user scroll back down the toolbar don't come back. This is my code.
public class QA_Activity extends Fragment {
private static final String TAG = QA_Activity.class.getSimpleName();
private ListView listView;
private QAFeedListAdapter listAdapter;
private List<QAFeedItem> feedItems;
private String URL_FEED = "http://localhost/quesans.json";
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.list_activity, container, false);
listView = (ListView) v.findViewById(R.id.list);
feedItems = new ArrayList<QAFeedItem>();
listAdapter = new QAFeedListAdapter(getActivity(), feedItems);
listView.setAdapter(listAdapter);
listView.setOnScrollListener(new OnScrollListener() {
private int mLast;
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (mLast < firstVisibleItem) {
if (((AppCompatActivity)getActivity()).getSupportActionBar().isShowing()) {
((AppCompatActivity)getActivity()).getSupportActionBar().hide();
}
}
if (mLast > firstVisibleItem) {
if (((AppCompatActivity)getActivity()).getSupportActionBar().isShowing()) {
((AppCompatActivity)getActivity()).getSupportActionBar().show();
}
}
mLast = firstVisibleItem;
}
});
JsonObjectRequest jsonReq = new JsonObjectRequest(Method.GET,
URL_FEED, null, new Response.Listener<JSONObject>() {
#Override
public void onResponse(JSONObject response) {
VolleyLog.d(TAG, "Response: " + response.toString());
if (response != null) {
parseJsonFeed(response);
}
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
VolleyLog.d(TAG, "Error: " + error.getMessage());
}
});
AppController.getInstance().addToRequestQueue(jsonReq);
return v;
}
private void parseJsonFeed(JSONObject response) {
try {
JSONArray feedArray = response.getJSONArray("questionanswer");
for (int i = 0; i < feedArray.length(); i++) {
JSONObject feedObj = (JSONObject) feedArray.get(i);
QAFeedItem item = new QAFeedItem();
item.setId(feedObj.getInt("id"));
item.setName(feedObj.getString("question"));
item.setStatus(feedObj.getString("answer"));
feedItems.add(item);
}
listAdapter.notifyDataSetChanged();
} catch (JSONException e) {
e.printStackTrace();
}
}
}
What i want is when the user scroll down the toolbar should show up.
You can do is
UPDATE
Create a layout for the toolbar
layout/your_toolbar_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<android.support.v7.widget.Toolbar
android:id="#+id/your_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
And inflate the toolbar layout
LayoutInflater inflater = getLayoutInflater();
View view = inflater.inflate(R.layout.your_toolbar_layout, null);
Toolbar toolbar = view.findViewById(R.id.your_toolbar); //your reference to the toolbar
listView.addHeaderView(view)
Are you Looking for something like this?
If so, you can use design support library elements like CoordinatorLayout and AppBarLayout.
For more details, refer to this tutorial.

Categories