BlazorServer how to unsubscribe/Dispose from JSR invokled Js-Events? - blazor-server-side

Hi I have a 'user is inactiv -> logout' feature in my blazor server app. Via JSR I reset a Timer everytime the user presses a Mousebtn or a Key. If no 'ínput' is done the timer elapses and the logout methode is called (in this example the counterpage is navigated to).
I included this component in the Blazor shared/mainlayout.razor.
But the logout methode is getting called even if the user is on a page that doesn't use the mainlayout anymore (e.g. logoutpage // no mainlayout).
How do I stop the once invoked JSR Event to call the logoutmethode?
What scope has the mainlayout.razor?
If I don't include the logoff component in the mainlayout and instead use it on a 'normal' page as a component it works a expected.
Code:
MainLayout.razor:
#inherits LayoutComponentBase
<PageTitle>InActivLogOffComponent</PageTitle>
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="sticky-md-top bg-white"><MyHeader></MyHeader></div>
<article class="content px-4">
#Body
</article>
</main>
</div>
TimerComponent:
#using System.Timers
#inject IJSRuntime _jsRuntime
#inject NavigationManager _navigationManager
<div class="border border-2 border-primary m-2 p-2">
<h3>MyHeaderPage</h3>
</div>
#code {
protected override void OnInitialized()
{//the JSR-Event seems to reinstantiate this code ???
Ini();
}
public System.Timers.Timer timerObj = new();
public async void Ini()
{//Set Timer
timerObj = new System.Timers.Timer(3000);
timerObj.Elapsed += EventHandlerTimerElapsed;
timerObj.AutoReset = false;
timerObj.Start();
//observe onclick and onkeypress in JS function
await _jsRuntime.InvokeVoidAsync("timeOutCall", DotNetObjectReference.Create(this));
}
[JSInvokable]
public void TimerIntervalReset()
{// Resetting the Timer
timerObj.Stop();
timerObj.Start();
}
public void EventHandlerTimerElapsed(Object? source, ElapsedEventArgs e)
{//if Timer is elapsed ... (no mouseclick or keypress happend for XX time)
//PROBLEM NOTHING Stops the Event from happening again and again
timerObj.Elapsed -= EventHandlerTimerElapsed;
timerObj.Enabled = false;
timerObj.Dispose();
//^^
_navigationManager.NavigateTo("/counter", true);
}
}
My Javascript:
function timeOutCall(dotnethelper) {
//document.onmousemove = resetTimeDelay;
document.onclick = resetTimeDelay;
document.onkeypress = resetTimeDelay;
function resetTimeDelay() {
dotnethelper.invokeMethodAsync("TimerIntervalReset");
}
}

Related

Could not set or bind model property with Bootstrap Datepicker in Blazor

I am using bootstrap datepicker and the problem is that when I pick a date, it does not fire a change or input event and noting is binding with the model property Course.StartDate or Course.EndDate.
The default datepicker works but does not support Afghanistan datetime. That is why I use boostrap datepicker.
Blazor code:
#using Microsoft.AspNetCore.Mvc.Rendering
#using myproject.Data
#using Microsoft.JSInterop;
#inject myproject.Repository.CoursesRepository _coursesRepository
#inject IJSRuntime JS
<EditForm Model="#Course" OnValidSubmit="e=> { if(selectedId == 0) { addCourse(); } else { updateCourse(Course.CourseId); } }">
<div class="mb-2">
<div>#Course.StartDate</div>
<label class="col-form-label" for="StartDate">#Loc["Start Date"]<span class="text-danger fs--1">*</span>:</label>
<InputDate class="form-control" #bind-Value="Course.StartDate" #bind-Value:format="yyyy-MM-dd" id="StartDate" />
<ValidationMessage class="text-danger" For="(() => Course.StartDate)"/>
</div>
<div class="mb-2">
<label class="col-form-label" for="EndDate">#Loc["End Date"]<span class="text-danger fs--1">*</span>:</label>
<InputDate class="form-control" #bind-Value="Course.EndDate" #bind-Value:format="yyyy-MM-dd" id="EndDate"/>
<ValidationMessage class="text-danger" For="(() => Course.EndDate)"/>
</div>
</EditForm>
#code {
public CourseModel Course = new();
public string[] dates = new string[] { "#StartDate", "#EndDate" };
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
loadScripts();
}
void addCourse()
{
_coursesRepository.AddCourse(Course);
FillData();
Course = new();
var title = "Course";
Swal.Success(title : Loc[$"{title} added successfully"],toast : true);
}
// initializes the datepicker
public async Task loadScripts()
{
await JS.InvokeVoidAsync("initializeDatepicker", (object) dates);
}
}
This is script for initializing the datepickers
<script>
function initializeDatepicker(dates) {
dates.forEach((element) => {
$(element).datepicker({
onSelect: function(dateText) {
// this is not working
element.value = this.value;
/*
tried this and still not working
$(element).trigger("change");
also tried this and still not working
$(element).change();
*/
// this is working
console.log("Selected date: " + dateText + "; input's current value: " + this.value);
},
dateFormat: 'yy-mm-dd',
changeMonth: true,
changeYear: true
});
});
}
</script>
The reason for this is that the changes are made with JavaScript and so the page state does not change for Blazor, in other words, Blazor does not notice the value change at all.
To solve this problem, you must inform the Blazor component of the changes by calling a C# method inside the JavaScript function. For this, you can use the DotNet.invokeMethodAsync built-in dotnet method. As follows:
DotNet.invokeMethodAsync('ProjectAssemblyName', 'ComponentMethod', this.value.toString())
Its first argument is the assembly name of your project. The second argument is the name of the C# function that you will write in the component, and finally, the third argument is the selected date value.
The method called in C# should be as follows:
static string selectedDate;
[JSInvokable]
public static void ComponentMethod(string pdate)
{
selectedDate = pdate;
}
This method must be decorated with [JSInvokable] and must be static.
I have done the same thing for another javascript calendar in Persian language. Its codes are available in the JavaScriptPersianDatePickerBlazor repository.
You can also create a custom calendar in the form of a component so that you can use it more easily in all components in any formats that you want such as DateTime or DateTimeOffset or string and so on. There is an example of this in the AmibDatePickerBlazorComponent repository.

Spinner does not show b/c the bound variable is not updated

I'm working on a Blazor server side app. The page has a table with a list of cars and some filter elements on top. When I select a filter, a spinner should be visible until the new data is fetched and rendered.
The spinner with its variable:
<div class="spinner-border #spinner" role="status">
<span class="visually-hidden">Loading...</span>
</div>
#code{
string spinner = "invisible";
public string vehicleTypeFilter
{
set
{
_vehicleTypeFilter = value;
ApplyFilters();
}
get { return _vehicleTypeFilter; }
}
}
The select for the Baumuster (vehicleType) is bound to the vehicleTypeFilter variable:
<div class="col-md-2 form-floating">
<select class="form-control" #bind="vehicleTypeFilter">
<option value="" selected>Alle</option>
#foreach (var vehicleType in vehicleTypes.OrderBy(x => x.Description))
{
<option value="#vehicleType.Description">#vehicleType.Description</option>
}
</select>
<label>Baumuster</label>
</div>
Then a value is selected, the ApplyFilter method is triggered through the setter of the vehicleTypeFilter variable:
public void ApplyFilters()
{
ToggleSpinner();
// I also tried a StateHasChanged(); right here
// 1. Get all cars
cars = model.CreateIndexViewModel();
// 2. Filter for Baumuster / vehicle type
if (!string.IsNullOrEmpty(vehicleTypeFilter))
{
cars.viewModels = cars.viewModels.Where(x => x.VehicleDescription == vehicleTypeFilter).ToList();
}
ToggleSpinner();
}
The ToggleSpinner method:
public void ToggleSpinner()
{
if (spinner == "invisible" )
spinner = "";
else
spinner = "invisible";
}
Unfortunately, I don't see the spinner. When I inspect the html page right after the breakpoint hits the Baumuster-filter, the value of spinner is still set to "invisible". I even tried to call StateHasChanged(); after the first ToggleSpinner() but that didn't help.
You've shown a lot of code, but I don't see ToggleSpinner
However, you call it twice in your ApplyFilters method, with no blocking calls, so I'd assume that it's turning the spinner on and off so fast that it doesn't render (or at least that you can't notice it).
If the methods you call in ApplyFilters actually take any time, then Henk's got the right idea-- except you should use async Task I think.
Your problem is that you want async behaviour from a synchronous property. The standard advice is against async void but if you want to stay with the property, the minimal change would be:
public async void ApplyFilters()
{
ToggleSpinner();
// I also tried a StateHasChanged(); right here
StateHasChanged(); // this _requests_ an update
await Task.Delay(1); // this is why you need async void
... as before
ToggleSpinner();
StateHasChanged();
}

livewire view don't send any xhr request to component

I have a select html tag in livewire view:
<select wire:model="categoryId" wire:change="$emit('attr_cat', $event.target.value)" name="category_id" class="form-control">
<option value="0">Select category</option>
#foreach($categories as $category)
<option value="{{$category->id}}">{{ $category->name }}</option>
#if(count($category->childs))
#include('admin.categories.subcategorylist',['childs' => $category->childs])
#endif
#endforeach
</select>
and a livewire component with following code:
namespace App\Http\Livewire\Admin;
use App\Models\Attribute;
use App\Models\Category;
use App\Models\CategoryAttribute;
use Barryvdh\Debugbar\Facade as Debugbar;
use Livewire\Component;
class CategoryAttributes extends Component
{
public $attributeId;
public $categoryId;
public $attributeCategories;
public $categories;
protected $listeners = ['attr_cat' => 'addAttributeToCategory'];
public function mount()
{
$this->attributeCategories = Attribute::find($this->attributeId)->categories()->get();
$this->categories = Category::whereNull('category_id')->orderBy('name')->with('childs')->get();
$this->categoryId = 0;
}
public function render()
{
return view('livewire.admin.category-attributes');
// return view('livewire.admin.cat-attr-test');
}
public function addAttributeToCategory($value){
Debugbar::addMessage($value);
CategoryAttribute::create([
'category_id' => $this->categoryId,
'attribute_id' => $this->attributeId,
]);
}
public function updatedCategoryId(){
Debugbar::addMessage($this->attributeId);
}
}
and mount livewire component with this line in my blade view:
#livewire('admin.category-attributes', ['attributeId' => $attribute->id], key('attr-'.$attribute->id))
but, when I change selected item, nothing happens and no xhr requests are sent to the server by the browser.
Let me say this too, I have inserted #livewireStyles and #livewireScripts in master blade file and they are loaded correctly.
Thank you for your help.
I found the solution to my problem. I had to put all the contents of the view in a standalone tag so that livewire could assign an ID to it. Otherwise, livewire will refuse to send any request to the server. 

Blazor: How to get sender from event in child component

I have made a component with an event as described in the docs
If I use multiple instances of the component, is there any way I can tell which instance it was that fired the event?
In "regular" .net there usually is a sender parameter.
See my BlazorFiddle
... or look at my sample code here:
ChildComponent
<div class="panel panel-default">
<div class="panel-heading">#Title</div>
<div class="panel-body">#ChildContent</div>
<button class="btn btn-primary" #onclick="OnClick">
Trigger a Parent component method
</button>
</div>
#code {
[Parameter]
private string Title { get; set; }
[Parameter]
private RenderFragment ChildContent { get; set; }
[Parameter]
private EventCallback<UIMouseEventArgs> OnClick { get; set; }
}
Index
#page "/"
<ChildComponent Title="Panel Title from Parent"
OnClick="#ShowMessage">Hello child 1</ChildComponent>
<br>
<br>
<ChildComponent Title="Panel Title from Parent"
OnClick="#ShowMessage">Hello child 2</ChildComponent>
<br>
<br>
<p><b>#messageText</b></p>
#code {
private string messageText;
private void ShowMessage(UIMouseEventArgs e)
{
// How do I get which ChildComponent was the sender?
messageText = DateTime.Now.ToString() + ": Message from child ?"
}
}
I want to be able to get data from the sending ChildComponent in the ShowMessage-function in Index.
Unfortunately you don't have a sender and this topic was already discussed here: https://github.com/aspnet/Blazor/issues/1277.
No. That would involve creating some new way to track the identity of
a DOM element.
If you were able to describe at a more high level what sort of
functionality you're trying to implement, it might be there's a more
idiomatically Blazor-ish way to achieve what you want simply.
The workaround for it is to explicitly pass parameters (event & others) to your ShowMessage method.
#page "/"
<ChildComponent Title="Panel Title from Parent"
OnClick="#((ev) => ShowMessage(ev, 1))">Hello child 1</ChildComponent>
<br>
<br>
<ChildComponent Title="Panel Title from Parent"
OnClick="#((ev) => ShowMessage(ev, 2))">Hello child 2</ChildComponent>
<br>
<br>
<p><b>#messageText</b></p>
#code {
private string messageText;
// ex: Blazor 0.7 (Old)
private void ShowMessage(UIMouseEventArgs e, int childId)
{
// How do I get which ChildComponent was the sender?
messageText = DateTime.Now.ToString() + ": Message from child " + childId.ToString();
}
// ex: .NET Core 3.0 (Updated)
// UIMouseEventsArgs were removed
// Replace Microsoft.AspNetCore.Components.UIEventArgs with System.EventArgs and remove the “UI” prefix from all EventArgs derived types (UIChangeEventArgs -> ChangeEventArgs, etc.).
// https://devblogs.microsoft.com/aspnet/asp-net-core-and-blazor-updates-in-net-core-3-0-preview-9/
private void ShowMessage(MouseEventArgs e, int childId)
{
// How do I get which ChildComponent was the sender?
messageText = DateTime.Now.ToString() + ": Message from child " + childId.ToString();
}
}
Old BlazorFiddler here (Blazor 0.7): https://blazorfiddle.com/s/9zh85obk
Updated BlazorFiddler here (.NET Core 3.0): https://blazorfiddle.com/s/5p45emmo
In order to track if there will be any updates on this, you can follow this issue: https://github.com/aspnet/AspNetCore/issues/5501

Switch to at Iframe in selenium java

I am new using selenium and I am trying to test a web page. This page has as iframe and for the read I need do a switch, the switch I did was:
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(frame));
But it does not works, maybe somebody should be help me? Here is my code. this code of my page:
public class MainPage extends BasePageObject{
#FindBy(id = "cc-sa-item-logo")
WebElement iconLogin;
private By frame = By.xpath("//iframe[#id='cms']");
public MainPage() {
PageFactory.initElements(driver, this);
waitUntilPageObjectIsLoaded();
}
#Override
public void waitUntilPageObjectIsLoaded() {
wait.until(ExpectedConditions.visibilityOf(iconLogin));
}
public boolean logOutExists() {
boolean res = true;
String parentWindow = driver.getWindowHandle();
try{ wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(frame));
res = iconLogin.isDisplayed();
}finally {
driver.switchTo().window(parentWindow);
}
return res;
}
and this is the code of page the test:
<iframe id="cms" class="cms" src="/app/s9cea6fcedc36dd82/p430c9b54081c966d/" onload="this.loaded = true;" name="cms">
<!DOCTYPE html>
<html lang="es-ES">
<head>
<body id="page-2281940529" class="body cc-page cc-page-index cc-indexpage j-m-flash-styles j-m-gallery-styles j-m-video-styles j-m-hr-styles j-m-header-styles j-m-text-styles j-m-emotionheader-styles j-m-htmlCode-styles j-m-rss-styles j-m-form-styles-disabled j-m-table-styles j-m-textWithImage-styles j-m-downloadDocument-styles j-m-imageSubtitle-styles j-m-flickr-styles j-m-googlemaps-styles j-m-blogSelection-styles-disabled j-m-comment-styles j-m-jimdo-styles j-m-profile-styles j-m-guestbook-styles j-m-promotion-styles j-m-twitter-styles j-m-hgrid-styles j-m-shoppingcart-styles j-m-catalog-styles j-m-product-styles-disabled j-m-facebook-styles j-m-sharebuttons-styles j-m-externalSource-styles j-m-formnew-styles-disabled j-m-callToAction-styles j-m-turbo-styles j-m-spacing-styles j-m-googleplus-styles j-m-dummy-styles j-footer-styles cc-content-parent" style="background-position: -186px 0px;">
<div class="cc-bs cc-alert-container" ng-controller="cms.common.AlertCtrl"> </div>
<div id="cc-eh" data-display="cms-only">
<div id="cc-nav" class="cc-jimdo ui-draggable" data-display="cms-only">
<script>
<div id="cc-clipboard" class="cc-clipboard-scrolled-left cc-clipboard-scrolled-right cc-clipboard-empty" data-display="cms-only" style="right: 186px; display: block;">
<div id="cc-inner" class="cc-content-parent" style="padding-right: 186px; padding-top: 0px;">
<div id="cc-sa" data-display="cms-only" style="display: block;">
<div id="cc-sa-sidebar" ng-controller="cms.sa.SidebarCtrl">
<div class="cc-sa-sidebar-group cc-sa-sidebar-group--top">
<div class="cc-sa-sidebar-logo-open clear">
<span id="cc-sa-item-logo" class="cc-sa-sidebar-logo" data-params="/app/siteadmin/upgrade/index/,true" data-action="sidebarItem"> </span>
I need find the element of span tag
The issue here is that the first thing you do, after initializing the FindBy-annotated fields, is waitUntilPageObjectIsLoaded, which is implicitly calling driver.findElement(By.id("cc-sa-item-logo")).
And here it fails, as you haven't switched to the frame yet at this point, and unfortunately PageFactory is not able to handle that.
Basically you need to wrap each and every call to a FindBy-annotated field with a switchTo().frame/switchTo().defaultContent(). Just like you did in logOutExists.
For convenience, I would model the iframe in a separate page object (class), so as to encapsulate (and automate) the switching back and forth.
public class CmsIframe extends BasePageObject implements AutoCloseable {
#FindBy(id = "cc-sa-item-logo")
WebElement iconLogin;
private By frame = By.xpath("//iframe[#id='cms']");
public CmsIframe() {
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(frame));
PageFactory.initElements(driver, this);
waitUntilPageObjectIsLoaded();
}
#Override
public void waitUntilPageObjectIsLoaded() {
wait.until(ExpectedConditions.visibilityOf(iconLogin));
}
public boolean logOutExists() {
return iconLogin.isDisplayed();
}
#Override
public void close() {
driver.switchTo().defaultContent();
}
}
MainPage can then be re-written as follows:
public class MainPage extends BasePageObject{
private By frame = By.xpath("//iframe[#id='cms']");
public MainPage() {
PageFactory.initElements(driver, this);
}
public boolean logOutExists() {
try (CmsIframe cmsIframe = new CmsIframe()) {
return cmsIframe.logOutExists();
} // when exiting this try block, the driver will automatically be switched back out of the iframe (as part of CmsIframe#close)
}
}
Please note that thanks to implements AutoCloseable, CmsIframe can be used in a try-with-resources Statement (java > 7 only), which sorts of automates the switching back to the default content.
PS: if you could post the stacktrace with the exception you are getting, that would help help you.
I finally solved my problem, you had reason the problem was that I find the switch in the place incorrect. the solution was:
#Override
public void waitUntilPageObjectIsLoaded() {
String parentWindow = driver.getWindowHandle();
try{
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(frame));
wait.until(ExpectedConditions.visibilityOf(iconLogin));
}finally {
driver.switchTo().window(parentWindow);
}
}
public boolean logOutExists() {
boolean res = true;
String parentWindow = driver.getWindowHandle();
try{
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(frame));
res = iconLogin.isDisplayed();
}finally {
driver.switchTo().window(parentWindow);
}
return res;
}