How to create an Eventcallback which has the arguments in blazor? - c#

I need to create a Eventcallback for this following method
public async Task methodA(bool isLoading)
{
IsLoading = isLoading;
this.StateHasChanged();
await Task.CompletedTask;
}
I've tried it using following
EventCallback callback=> EventCallback.Factory.Create(this, methodA);
but I am getting a error called
can't convert method group to EventCallback
I need to create this event callback and pass it to the child components using cascading parameters as following
<CascadingValue Value=callback>
#Body
</CascadingValue>
#code{
EventCallback callback=> EventCallback.Factory.Create(this, methodA);
public async Task methodA(bool isLoading)
{
IsLoading = isLoading;
this.StateHasChanged();
await Task.CompletedTask;
}
}
so what is wrong with above code ??and how to create an eventcallback for above method?

Related

Blazor async cascading value always null

Let's say I have the following code in MainLayout.razor
<CascadingValue Value="#(async () => await someclass.GetValueAsnyc())" Name="asyncValue">
<article class="content px-4">
#Body
</article>
</CascadingValue>
And in Index.razor:
<h1>Hello!</h1>
#code{
[CascadingParameter(Name = "asyncValue")] public string AsyncValue{ get; set; }
protected override async Task OnInitializedAsync()
{
//do something with AsyncValue- it is always null
}
}
My assumption is that the declaration of the variable in Index.razor isn't awaiting the cascading value in Mainlayout.razor. It does work is I call the method synchronsly with .Result at the end, but that's not what I would like to do if possible.
Does anyone have any guidance or can point me in the right direction so that the cascading value loads asynchronously?
I think this should help.
First a consumer component. The cascaded value is a Task which may or may not have completed. To consume it you await it. If it's already completed the there's no awaiting to do. It provides the result immediately.
<h3>#this.value</h3>
<button class="btn btn-primary" #onclick=this.OnClick>Get Value Again</button>
#code {
private string value = string.Empty;
[CascadingParameter] private Task<string>? DataTask { get; set; }
protected override async Task OnInitializedAsync()
{
if (this.DataTask is not null)
this.value = await DataTask;
}
private async Task OnClick()
{
if (this.DataTask is not null)
this.value = await DataTask;
}
}
And a test page. I haven't put it in a layout or App as it's easier to see what's happening in a test page. The cascaded value is a Task<string> field that you assign a method to.
#page "/Test"
<PageTitle>Test</PageTitle>
<CascadingValue Value="this.DataTask">
<MyComponent />
</CascadingValue>
#code {
private Task<string>? DataTask;
protected override Task OnInitializedAsync()
{
DataTask = GetSomeDateAsync();
return Task.CompletedTask;
}
private async Task<string> GetSomeDateAsync()
{
await Task.Delay(5000);
return DateTime.Now.ToLongTimeString();
}
}
This is basically how the AuthenticationState cascade works.

onpaste input event in blazor and typescript

The #onpaste is a blazor DOM event executed when content is pasted in an input.
now, I want to get this text content from typescript (blazor doesn't support it).
razor:
#inject IJSRuntime _Js
<input class="otp-input"
#ref="myInputRef"
#onpaste="HandleOnPaste" />
#code {
private ElementReference myInputRef { get; set; }
private async Task HandleOnPaste()
{
var pastedData = await _js.InvokeAsync<string?>("OtpInput.getPastedData")
// DO Something
}
}
ts:
class OtpInput {
static getPastedData(e: ClipboardEvent) {
let pastedData = e.clipboardData!.getData('text');
return pastedData;
}
}
this does not work and return error: TypeError: Cannot read properties of undefined (reading 'clipboardData')
You can use a different approach. Add event listener to the input element using javascript then call dotnet method and pass the event data. I use javascript but you can modify accordingly for typescript.
pasteInteropHelper.js:
export function addOnPasteEventListener(elem, componentInstance) {
elem.onpaste = (e) => {
var text = e.clipboardData.getData('text');
componentInstance.invokeMethod('HandlePaste', text);
}
}
razor component:
#inject IJSRuntime JS
<input #ref="_inputRef" />
#code {
private ElementReference _inputRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./pasteInteropHelper.js");
var selfReference = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("addOnPasteEventListener", _inputRef, selfReference);
}
}
[JSInvokable]
public void HandlePaste(string text)
{
Console.WriteLine(text);
}
}

How do I await an async Task from the oninput event of a textbox in Blazor?

My Blazor component looks like this:
<input type="text" #bind-value="#MyText" #bind-value:event="oninput" />
#code {
private String myText;
protected String MyText
{
get { return myText; }
set
{
myText = value;
//await GetMyHttp();
}
}
private async Task GetMyHttp()
{
HttpClient Http = _httpClientFactory.CreateClient("MyId");
myData = await Http.GetFromJsonAsync<MyData>("MyParams");
}
}
And this works fine. But if I uncomment the await GetMyHttp(); line, I get this compilation error:
Error CS4033 The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
How do I refactor this code to await the async method when the user types in the text box?
You can have your MyText property getter and setter like this
protected String MyText
{
get => myText;
set
{
myText = value;
Task.Run(()=> GetMyHttp());
}
}
Alternatively you can hook into component's SetParametersAsync lifecycle event which will be called automatically when parameters are set for component, it will call GetMyHttp only if value of MyText is changed
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>("MyText", out var value))
{
if (value != MyText)
{
await GetMyHttp();
}
}
await base.SetParametersAsync(parameters);
}

How to get notify when a user closes the browser in SignalR Blazor?

I want to send a notification to all the users connected in a group when a user lefts. How can I do that?
As you can see in my code I've overridden the OnDisconnectedAsync method, but it works only when I call HubConnection.DisposeAsync();. But if I close the browser or tab, it does nothing.
public override Task OnConnectedAsync()
{
Console.WriteLine("User Joined.");
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
Console.WriteLine("User Left.");
return base.OnDisconnectedAsync(exception);
}
Is there any way to get notified when the browser is closed?
You can create a service class that derives from CircuitHandler:
CircuitHandlerService.cs
using Microsoft.AspNetCore.Components.Server.Circuits;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public class CircuitHandlerService : CircuitHandler
{
public ConcurrentDictionary<string, Circuit> Circuits { get; set; }
public event EventHandler CircuitsChanged;
protected virtual void OnCircuitsChanged()
=> CircuitsChanged?.Invoke(this, EventArgs.Empty);
public CircuitHandlerService()
{
Circuits = new ConcurrentDictionary<string, Circuit>();
}
public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
{
Circuits[circuit.Id] = circuit;
OnCircuitsChanged();
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
{
Circuit circuitRemoved;
Circuits.TryRemove(circuit.Id, out circuitRemoved);
OnCircuitsChanged();
return base.OnCircuitClosedAsync(circuit, cancellationToken);
}
public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
return base.OnConnectionDownAsync(circuit, cancellationToken);
}
public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
return base.OnConnectionUpAsync(circuit, cancellationToken);
}
}
Add the service to the DI container:
services.AddSingleton<CircuitHandler>(new CircuitHandlerService());
Test the service:
#page "/"
#using Microsoft.AspNetCore.Components.Server.Circuits
#inject CircuitHandler circuitHandler
#implements IDisposable
<h1>Hello, world!</h1>
Welcome to your new app.
<p>
Number of Circuits: #((circuitHandler as BlazorCircuitHandler.Services.CircuitHandlerService).Circuits.Count)
<ul>
#foreach (var circuit in (circuitHandler as BlazorCircuitHandler.Services.CircuitHandlerService).Circuits)
{
<li>#circuit.Key</li>
}
</ul>
</p>
#code {
protected override void OnInitialized()
{
// register event handler
(circuitHandler as CircuitHandlerService).CircuitsChanged += HandleCircuitsChanged;
}
public void Dispose()
{
// unregister the event handler when the component is destroyed
(circuitHandler as CircuitHandlerService).CircuitsChanged -= HandleCircuitsChanged;
}
public void HandleCircuitsChanged(object sender, EventArgs args)
{
// notify the UI that the state has changed
InvokeAsync(() => StateHasChanged());
}
}
Use CircuitHandler
Blazor Server allows code to define a circuit handler , which allows running code on changes to the state of a user's circuit. A circuit handler is implemented by deriving from CircuitHandler and registering the class in the app's service container.
public class TrackingCircuitHandler : CircuitHandler
{
private HashSet<Circuit> circuits = new HashSet<Circuit>();
public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
circuits.Add(circuit);
return Task.CompletedTask;
}
public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
circuits.Remove(circuit);
return Task.CompletedTask;
}
public int ConnectedCircuits => circuits.Count;
}
Circuit handlers are registered using DI
services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();

Display wait or spinner on API call

In my Blazor app I am making an API call to a back end server that could take some time. I need to display feedback to the user, a wait cursor or a "spinner" image. How is this done in Blazor?
I have tried using CSS and turning the CSS on and off but the page is not refreshed until the call is completed. Any suggestions would be greatly appreciated.
#functions {
UserModel userModel = new UserModel();
Response response = new Response();
string errorCss = "errorOff";
string cursorCSS = "cursorSpinOff";
protected void Submit()
{
//Show Sending...
cursorCSS = "";
this.StateHasChanged();
response = Service.Post(userModel);
if (response.Errors.Any())
{
errorCss = "errorOn";
}
//turn sending off
cursorCSS = "cursorSpinOff";
this.StateHasChanged();
}
}
Option 1: Using Task.Delay(1)
Use an async method.
Use await Task.Delay(1) or await Task.Yield(); to flush changes
private async Task AsyncLongFunc() // this is an async task
{
spinning=true;
await Task.Delay(1); // flushing changes. The trick!!
LongFunc(); // non-async code
currentCount++;
spinning=false;
await Task.Delay(1); // changes are flushed again
}
Option 1 is a simple solution that runs ok but looks like a trick.
Option 2: Using Task.Run() (not for WebAssembly)
On January'2020. #Ed Charbeneau published BlazorPro.Spinkit project enclosing long processes into task to don't block the thread:
Ensure your LongOperation() is a Task, if it is not, enclose it into a Task and await for it:
async Task AsyncLongOperation() // this is an async task
{
spinning=true;
await Task.Run(()=> LongOperation()); //<--here!
currentCount++;
spinning=false;
}
Effect
Spinner and server side prerendering
Because Blazor Server apps use pre-rendering the spinner will not appear, to show the spinner the long operation must be done in OnAfterRender.
Use OnAfterRenderAsync over OnInitializeAsync to avoid a delayed server-side rendering
// Don't do this
//protected override async Task OnInitializedAsync()
//{
// await LongOperation();
//}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Run(()=> LongOperation());//<--or Task.Delay(0) without Task.Run
StateHasChanged();
}
}
More samples
Learn more about how to write nice spinner you can learn from open source project BlazorPro.Spinkit, it contains clever samples.
More Info
See Henk Holterman's answer with blazor internals explanation.
Lot's of great discussion surrounding StateHasChanged(), but to answer OP's question, here's another approach for implementing a spinner, universally, for HttpClient calls to a backend API.
This code is from a Blazor Webassembly app...
Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<SpinnerService>();
builder.Services.AddScoped<SpinnerHandler>();
builder.Services.AddScoped(s =>
{
SpinnerHandler spinHandler = s.GetRequiredService<SpinnerHandler>();
spinHandler.InnerHandler = new HttpClientHandler();
NavigationManager navManager = s.GetRequiredService<NavigationManager>();
return new HttpClient(spinHandler)
{
BaseAddress = new Uri(navManager.BaseUri)
};
});
await builder.Build().RunAsync();
}
SpinnerHandler.cs
Note: Remember to uncomment the artificial delay. If you use the out-of-the-box Webassembly template in Visual Studio, click the Weather Forecast to see a demo of the spinner in action.
public class SpinnerHandler : DelegatingHandler
{
private readonly SpinnerService _spinnerService;
public SpinnerHandler(SpinnerService spinnerService)
{
_spinnerService = spinnerService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_spinnerService.Show();
//await Task.Delay(3000); // artificial delay for testing
var response = await base.SendAsync(request, cancellationToken);
_spinnerService.Hide();
return response;
}
}
SpinnerService.cs
public class SpinnerService
{
public event Action OnShow;
public event Action OnHide;
public void Show()
{
OnShow?.Invoke();
}
public void Hide()
{
OnHide?.Invoke();
}
}
MainLayout.razor
#inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
About
</div>
<div class="content px-4">
#Body
<Spinner />
</div>
</div>
</div>
Spinner.razor
Note: To add some variety, you could generate a random number in the OnIntialized() method, and use a switch statement inside the div to pick a random spinner type. In this method, with each HttpClient request, the end user would observe a random spinner type. This example has been trimmed to just one type of spinner, in the interest of brevity.
#inject SpinnerService SpinnerService
#if (isVisible)
{
<div class="spinner-container">
<Spinner_Wave />
</div>
}
#code
{
protected bool isVisible { get; set; }
protected override void OnInitialized()
{
SpinnerService.OnShow += ShowSpinner;
SpinnerService.OnHide += HideSpinner;
}
public void ShowSpinner()
{
isVisible = true;
StateHasChanged();
}
public void HideSpinner()
{
isVisible = false;
StateHasChanged();
}
}
Spinner-Wave.razor
Credit to: https://tobiasahlin.com/spinkit/
Note: There is a Nuget package for this spin kit. The drawback to the Nuget package is that you don't have direct access to the CSS to make tweaks. Here I've tweaked thee size of the spinner, and set the background color to match the site's primary color, which is helpful if you are using a CSS theme throughout your site (or perhaps multiple CSS themes)
#* Credit: https://tobiasahlin.com/spinkit/ *#
<div class="spin-wave">
<div class="spin-rect spin-rect1"></div>
<div class="spin-rect spin-rect2"></div>
<div class="spin-rect spin-rect3"></div>
<div class="spin-rect spin-rect4"></div>
<div class="spin-rect spin-rect5"></div>
</div>
<div class="h3 text-center">
<strong>Loading...</strong>
</div>
<style>
.spin-wave {
margin: 10px auto;
width: 200px;
height: 160px;
text-align: center;
font-size: 10px;
}
.spin-wave .spin-rect {
background-color: var(--primary);
height: 100%;
width: 20px;
display: inline-block;
-webkit-animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
}
.spin-wave .spin-rect1 {
-webkit-animation-delay: -1.2s;
animation-delay: -1.2s;
}
.spin-wave .spin-rect2 {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.spin-wave .spin-rect3 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
.spin-wave .spin-rect4 {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.spin-wave .spin-rect5 {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
##-webkit-keyframes spin-waveStretchDelay {
0%, 40%, 100% {
-webkit-transform: scaleY(0.4);
transform: scaleY(0.4);
}
20% {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
##keyframes spin-waveStretchDelay {
0%, 40%, 100% {
-webkit-transform: scaleY(0.4);
transform: scaleY(0.4);
}
20% {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
</style>
It's beautiful
To answer the notice in #daniherrera's solution, there is three more elegant solution proposed here.
In short :
Implement INotifyPropertyChanged to the Model and invoke StateHasChanged() on a PropertyChangedEventHandler event property from the Model.
Use delegates to invoke StateHasChanged() on the Model.
Add a EventCallBack<T> parameter to the component or page of the View and assign it to the function that should change the render of the component and their parents. (StateHasChanged() isn't necessary in this one`)
The last option is the most simple, flexible and high level, but choose at your convenience.
Overall, I'll advise to use one of those solutions presented more than the await Task.Delay(1); one if security of your app is a concern.
Edit : After more reading, this link provide a strong explanation on how to handle events in C#, mostly with EventCallBack.
Don't do the same mistake as I did by testing wait spinner using Thread.Sleep(n).
protected override async Task OnInitializedAsync()
{
// Thread.Sleep(3000); // By suspending current thread the browser will freeze.
await Task.Delay(3000); // This is your friend as dani herrera pointed out.
// It creates a new task that completes
// after a specified number of milliseconds.
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
Not just for API call, but for every service call:
SpinnerService:
public class SpinnerService
{
public static event Action OnShow;
public static event Action OnHide;
public void Show()
{
OnShow?.Invoke();
}
public void Hide()
{
OnHide?.Invoke();
}
}
Spinner component:
Your spinner hier, in my case I have MudProgressCircular
#if (IsVisible)
{
<MudProgressCircular Color="Color.Primary"
Style="position: absolute;top: 50%;left: 50%;"
Indeterminate="true" />
}
#code{
protected bool IsVisible { get; set; }
protected override void OnInitialized()
{
SpinnerService.OnShow += ShowSpinner;
SpinnerService.OnHide += HideSpinner;
}
public void ShowSpinner()
{
IsVisible = true;
StateHasChanged();
}
public void HideSpinner()
{
IsVisible = false;
StateHasChanged();
}
}
ServiceCaller:
public class ServiceCaller
{
private readonly IServiceProvider services;
private readonly SpinnerService spinnerService;
public ServiceCaller(IServiceProvider services, SpinnerService spinnerService)
{
this.services = services;
this.spinnerService = spinnerService;
}
public async Task<TResult> CallAsync<TService, Task<TResult>>(Func<TService, TResult> method)
where TService : class
{
var service = this.services.GetRequiredService<TService>();
try
{
spinnerService.Show();
await Task.Delay(500); // ToDo: this line is not necessary
TResult? serviceCallResult = await Task.Run(() => method(service));
return serviceCallResult;
}
finally
{
spinnerService.Hide();
}
}
public async Task CallAsync<TService, TAction>(Func<TService, Action> method)
where TService : class
{
var service = this.services.GetRequiredService<TService>();
try
{
spinnerService.Show();
await Task.Delay(500); // ToDo: this line is not necessary
await Task.Run(() => method(service).Invoke());
}
finally
{
spinnerService.Hide();
}
}
}
How to use it?
#page "/temp"
#inject ServiceCaller serviceCaller;
<h3>Temp Page</h3>
<MudButton OnClick="CallMethodReturnsString">CallMethodReturnsString</MudButton>
<MudButton OnClick="CallVoidMethodAsync">CallVoidMethodAsync</MudButton>
<MudButton OnClick="CallTaskMethodAsync">CallTaskMethodAsync</MudButton>
<MudButton OnClick="CallMany">CallMany</MudButton>
#if (!string.IsNullOrEmpty(tempMessage)){
#tempMessage
}
#code{
string tempMessage = string.Empty;
// call method returns string
private async Task CallMethodReturnsString()
{
await serviceCaller.CallAsync<ITempService, string>(async x => this.tempMessage = await x.RetrieveStringAsync());
}
// call void method
private async Task CallVoidMethodAsync()
{
await serviceCaller.CallAsync<ITempService, Task>(x => () => x.MyVoidMethod());
}
// call task method
private async Task CallTaskMethodAsync()
{
await serviceCaller.CallAsync<ITempService, Task>(x => () => x.TaskMethod());
}
// call many methods
private async Task CallMany()
{
await serviceCaller.CallAsync<ITempService, Action>(x => async () =>
{
this.tempMessage = await x.RetrieveStringAsync();
x.MyVoidMethod();
x.TaskMethod();
});
}
}
use InvokeAsync(StateHasChanged), hopefully it will work.
protected async void Submit()
{
//Show Sending...
cursorCSS = "";
this.StateHasChanged();
response = Service.Post(userModel);
if (response.Errors.Any())
{
errorCss = "errorOn";
}
//turn sending off
cursorCSS = "cursorSpinOff";
await InvokeAsync(StateHasChanged);
}
Blazor Serverside - I needed to call StateHasChanged() to force the frontend to update so the spinner would show before the code moves onto the ajax call.
/* Show spinner */
carForm.ShowSpinner = true;
/* Force update of front end */
StateHasChanged();
/* Start long running API/Db call */
await _carRepository.Update(item);

Categories

Resources